# -*- coding: utf-8 -*-
"""
UTc! module: src.gameobjects

Purpose
=======
 Provides routines for creating and managing in-game objects.
 
Legal
=====
 This code is, in part, based on, but is largely dissimilar to, the LGPL code
 found at http://www.pygame.org/wiki/OBJFileLoader on February 9th, 2009.
 
 All other code, unless otherwise indicated, is original, and subject to the
 terms of the GPLv2, which is provided in COPYING.
 
 (C) Neil Tallim, 2009
"""
import math
import ode
import os
import pygame
import OpenGL.GL as GL
import OpenGL.GLU as GLU

import constants
import matrix
import session_constants

import config.parser
import rendering.rendering as rendering

_CLIP_MARGIN = -0.25 #: The percentage of the screen's size beyond which a margin should be established to prevent premature clipping. (Always negative)
_WIDTH_RENDER_MIN = None #: The smallest (negative) left-pixel-point at which objects should be rendered.
_WIDTH_RENDER_MAX = None #: The largest right-pixel-point at which objects should be rendered.
_HEIGHT_RENDER_MIN = None #: The smallest (negative) top-pixel-point at which objects should be rendered.
_HEIGHT_RENDER_MAX = None #: The largest bottom-pixel-point at which objects should be rendered.

_RETICLE_RANGE = 30 #: The offset from the centre of the screen within which objects will be considered under the reticle.

class GameObject(object):
	"""
	An abstract representation of any object that exists within game-space.
	
	Note: Many functions in this class return values computed by
	L{GameObject.updateCameraVectors}. If called before that function, which
	should be invoked each frame, the values they return will likely be useless.
	"""
	_camera_distance = None #: The distance between this GameObject and the camera.
	_camera_offset_x = None #: The position of this GameObject, relative to the camera, along the viewing X-axis.
	_camera_offset_y = None #: The position of this GameObject, relative to the camera, along the viewing Y-axis.
	_camera_offset_z = None #: The position of this GameObject, relative to the camera, along the viewing Z-axis.
	_onscreen = True #: True if the centre of this GameObject is on-screen.
	_pos_x = 0 #: The position of this GameObject, relative to the origin, along the X-axis.
	_pos_y = 0 #: The position of this GameObject, relative to the origin, along the Y-axis.
	_pos_z = 0 #: The position of this GameObject, relative to the origin, along the Z-axis.
	_render = True #: True if this GameObject should be rendered.
	_render_matrix = None #: The rotation matrix used to render this GameObject.
	_rotation_matrix = None #: The rotation matrix of this GameObject.
	_screen_position_x = None #: The X-coordinate at which this GameObject would be rendered.
	_screen_position_y = None #: The Y-coordinate at which this GameObject would be rendered.
	_under_reticle = False #: True if this GameObject appears under the central reticle.
	
	def __init__(self, rotation=None):
		"""
		Initialises the rotation of this object.
		
		@type rotation: sequence(3)|sequence(9)|None
		@param rotation: A Euler rotation set, a 3x3 rotation matrix, or None if
		    this object has no initial rotation and faces straight.
		
		@raise ValueError: If an unsupported rotation argument is supplied.
		"""
		if rotation is None:
			self.setRotation((0, 0, 0))
		elif len(rotation) == 3: #Euler.
			self.setRotation(rotation)
		elif len(rotation) == 9: #Matrix.
			self.setRotationMatrix(rotation)
		else:
			raise ValueError("Invalid rotation argument supplied.")
			
	def buildRenderMatrix(self):
		"""
		Updates and returns this object's render matrix.
		
		@rtype: tuple(12)
		@return: The 4x4 render matrix that specifies how this object should be
		    positioned in visual-space.
		"""
		self._render_matrix = matrix.buildRenderMatrix(self._rotation_matrix, (self._pos_x, self._pos_y, self._pos_z))
		return self._render_matrix
		
	def getDistance(self, position):
		"""
		Returns the distance between this object and the specified location in
		game-space.
		
		@type position: sequence(3)
		@param position: The (x, y, z) position against which distance is to be
		    calculated.
		
		@rtype: float
		@return: The distance between this object and the specified position.
		"""
		(x, y, z) = position
		return math.sqrt((self._pos_x - x) ** 2 + (self._pos_y - y) ** 2 + (self._pos_z - z) ** 2)
		
	def getPosition(self):
		"""
		Returns this object's position in game-space.
		
		@rtype: tuple(3)
		@return: The (x, y, z) co-ordinates of this object.
		"""
		return (self._pos_x, self._pos_y, self._pos_z)
		
	def getRotationEuler(self):
		"""
		Returns this object's rotation in Euler form.
		
		@rtype: tuple(3)
		@return: One possible (x, y, z) rotation of this object.
		"""
		return matrix.decomposeRotationMatrix(self._rotation_matrix)
		
	def getRotationMatrix(self):
		"""
		Returns this object's rotation matrix.
		
		@rtype: tuple(9)
		@return: This object's 3x3 rotation matrix.
		"""
		return self._rotation_matrix
		
	def getRenderMatrix(self):
		"""
		Returns this object's last-built render matrix.
		
		Note: If L{GameObject.buildRenderMatrix} wasn't called after updating this
		object's position, the returned value will likely be useless.
		
		@rtype: tuple(12)
		@return: This object's 4x4 render matrix.
		"""
		return self._render_matrix
		
	def getCameraDistances(self):
		"""
		Returns this object's position relative to the orientation and placement
		of the camera.
		
		@rtype: tuple(4)
		@return: This object's (x, y, z, dist) offsets from the camera's lens.
		"""
		return (self._camera_offset_x, self._camera_offset_y, self._camera_offset_z, self._camera_distance)
		
	def getScreenPosition(self):
		"""
		Returns the screen co-ordinates of the centre of this object.
		
		The origin, (0, 0), is the upper left corner of the screen.
		
		@rtype: tuple(2)
		@return: The (x, y) screen co-ordinates of this object.
		"""
		return (self._screen_position_x, self._screen_position_y)
		
	def isOnscreen(self):
		"""
		Indicates whether the centre of this object is currently on-screen.
		
		@rtype: bool
		@return: True if this object's centre is on-screen.
		"""
		return self._onscreen
		
	def isRenderable(self):
		"""
		Indicates whether the centre of this object is currently within the
		rendering margin.
		
		@rtype: bool
		@return: True if this object should be rendered.
		"""
		return self._render
		
	def isUnderReticle(self):
		"""
		Indicates whether the centre of this object is near the centre of the
		screen.
		
		@rtype: bool
		@return: True if this object should be considered under the reticle.
		"""
		return self._under_reticle
		
	def setPosition(self, position):
		"""
		Sets this object's absolute position in game-space.
		
		@type position: sequence(3)
		@param position: The (x, y, z) co-ordinates at which this object should be
		    placed.
		"""
		(self._pos_x, self._pos_y, self._pos_z) = position
		self._updateVectors(False)
		
	def setRotation(self, rotation):
		"""
		Sets this object's absolute rotation in Euler terms.
		
		@type rotation: sequence(3)
		@param rotation: The (x, y, z) rotation to apply to this object.
		"""
		(rot_x, rot_y, rot_z) = rotation
		self.setRotationMatrix(matrix.buildRotationMatrix(rot_x, rot_y, rot_z))
		
	def setRotationMatrix(self, rotation_matrix):
		"""
		Sets this object's absolute rotation by a rotation matrix.
		
		@type rotation_matrix: sequence(9)
		@param rotation_matrix: The 3x3 rotation matrix to apply to this object.
		"""
		self._rotation_matrix = rotation_matrix
		self._updateVectors(True)
		
	def updateCameraVectors(self, camera_transformation_matrix, process_location):
		"""
		Computes this object's position in visual-space, determining where it
		exists relative to the camera's position and orientation.
		
		@type camera_transformation_matrix: sequence(12)
		@param camera_transformation_matrix: The 4x4 transformation matrix that
		    represents the camera's current position and orientation.
		@type process_location: bool
		@param process_location: If true, this object's relative location will be
		    computed. There is no need to do this for non-player, non-missile
		    objects. (Added to reduce Python overhead, favouring OpenGL's culling)
		"""
		self.buildRenderMatrix()
		
		if process_location:
			(self._camera_offset_x, self._camera_offset_y, self._camera_offset_z) = matrix.multiply4x4Matrices(self._render_matrix, camera_transformation_matrix)[12:15]
			self._camera_offset_z *= -1
			
			self._camera_distance = math.sqrt(self._camera_offset_x ** 2 + self._camera_offset_y ** 2 + self._camera_offset_z ** 2)
			
			try:
				(self._screen_position_x, self._screen_position_y, win_z) = GLU.gluProject(self._pos_x, self._pos_y, self._pos_z)
				if self._camera_offset_z > 0.0:
					self._onscreen = 0 <= self._screen_position_x <= session_constants.WIDTH and 0 <= self._screen_position_y <= session_constants.HEIGHT
					self._render = self._onscreen or (_WIDTH_RENDER_MIN <= self._screen_position_x <= _WIDTH_RENDER_MAX and _HEIGHT_RENDER_MIN <= self._screen_position_y <= _HEIGHT_RENDER_MAX)
					self._under_reticle = abs(session_constants.WIDTH_HALF - self._screen_position_x) <= _RETICLE_RANGE and abs(session_constants.HEIGHT_HALF - self._screen_position_y) <= _RETICLE_RANGE
				else:
					self._onscreen = self._render = self._under_reticle = False
			except ValueError: #Attempted to project on camera: division by 0.
				self._onscreen = self._render = self._under_reticle = False
				self._screen_position_x = self._screen_position_y = 0
				
	def _updateVectors(self, rotation_update):
		"""
		A polymorphic stub for handling class-specific position/rotation update
		operations, such as keeping related elements in relative place.
		
		@type rotation_update: bool
		@param rotation_update: True if this object's rotation has changed.
		"""
		pass
		
		
class MobileGameObject(GameObject):
	"""
	An abstract representation of any non-stationary object that exists within
	game-space.
	"""
	_current_speed = 0 #: The current speed of this MobileGameObject.
	
	def __init__(self, rotation=None):
		"""
		See L{GameObject.__init__}.
		"""
		GameObject.__init__(self, rotation)
		
	def updateVectors(self, displacement, rotation):
		"""
		Sets the position and rotation of this object relative to its previous
		position and rotation.
		
		@type displacement: sequence(3)|None
		@param displacement: The (x, y, z) displacement of this object, or None
		    if no movement took place.
		@type rotation: sequence(3)|None
		@param rotation: The (x, y, z) rotation of this object, or None
		    if no rotation took place.
		"""
		if not displacement is None:
			(pos_x, pos_y, pos_z) = displacement
			self._pos_x += pos_x
			self._pos_y += pos_y
			self._pos_z += pos_z
			
		rotation_update = False
		if not rotation is None:
			(rot_x, rot_y, rot_z) = rotation
			if rot_x or rot_y or rot_z:
				self._rotation_matrix = matrix.multiply3x3Matrices(self._rotation_matrix, matrix.buildRotationMatrix(rot_x, rot_y, rot_z))
				rotation_update = True
				
		if displacement or rotation:
			#Update object-specific data, like ODE body rotation.
			self._updateVectors(rotation_update)
			
	def _move(self, seconds):
		"""
		Updates the position of this object by moving it forward based on its
		current speed.
		
		@type seconds: float
		@param seconds: The number of seconds at which the current speed was
		    sustained.
		
		@rtype: float
		@return: The distance travelled.
		"""
		distance_travelled = self._current_speed * seconds
		self.setPosition(matrix.computePosition(self._pos_x, self._pos_y, self._pos_z, 0, 0, distance_travelled, self._rotation_matrix))
		return distance_travelled
		
		
def init():
	"""
	Initialises the rendering margin around the edges of the screen. This
	function must be invoked before any other calls to this module.
	"""
	global _WIDTH_RENDER_MIN
	global _WIDTH_RENDER_MAX
	global _HEIGHT_RENDER_MIN
	global _HEIGHT_RENDER_MAX
	
	_WIDTH_RENDER_MIN = int(session_constants.WIDTH * _CLIP_MARGIN)
	_WIDTH_RENDER_MAX = session_constants.WIDTH - _WIDTH_RENDER_MIN
	_HEIGHT_RENDER_MIN = int(session_constants.HEIGHT * _CLIP_MARGIN)
	_HEIGHT_RENDER_MAX = session_constants.HEIGHT - _HEIGHT_RENDER_MIN
	
def loadOBJ(filename, mtl_override=None):
	"""
	This function processes an OBJ file.
	
	@type filename: basestring
	@param filename: The path of the OBJ file to load.
	@type mtl_override: sequence(3)|None
	@param mtl_override: A material-processing function, plus player and team
	    IDs.
	
	@rtype: tuple(5)
	@return: The procesed vertices, normals, faces, and materials.
	
	@raise Exception: If another error occurs, likely because of corrupt data.
	@raise IOError: If the specified OBJ file or its MTL file, if applicable,
	    cannot be read.
	"""
	_vertices = [] #A list of all vertices that comprise this model.
	_normals = [] #A list of all normals used to render this model.
	_faces = [] #A list of all polygonal faces that make up this model.
	_materials = None #A dictionary that describes how to render each material referenced by the OBJ.
 
	material = None #Name of the material to apply to any encountered faces.
	for line in open(filename, "r"):
		line = line.strip()
		if not line or line.startswith('#'): #Whitespace or comment.
			continue
			
		values = line.split()
		if values[0] == 'v': #Vertex.
			_vertices.append(map(float, values[1:4]))
		elif values[0] == 'vn': #Normals.
			_normals.append(map(float, values[1:4]))
		elif values[0] in ('usemtl', 'usemat'): #Update the material name.
			material = values[1]
		elif values[0] == 'f': #Face.
			face = []
			normals = []
			for v in values[1:]:
				w = v.split('/')
				face.append(int(w[0]))
				if len(w) >= 3 and len(w[2]) > 0:
					normals.append(int(w[2]))
				else:
					normals.append(0)
			_faces.append((face, normals, material))
		elif values[0] == 'mtllib':
			_materials = _loadMTL(values[1], os.path.dirname(filename), mtl_override)
			
	return (_vertices, _normals, _faces, _materials)
	
def _loadMTL(filename, pathname, mtl_override):
	"""
	This reads the contents of a MTL file.
	
	@type filename: basestring
	@param filename: The name of the MTL file to read.
	@type pathname: basestring.
	@param pathname: The path in which the MTL file is located.
	@type mtl_override: sequence(3)|None
	@param mtl_override: A material-processing function, plus player and team
	    IDs.
	
	@rtype: dict
	@return: The material dictionary generated by processing the MTL file.
	
	@raise IOError: If the specified OBJ file or its MTL file, if applicable,
	    cannot be read.
	@raise Exception: If another error occurs, likely because of corrupt data.
	"""
	contents = {} #The contents of the MTL file, keyed by material name.
	mtl = None #The material currently being defined.
	override = False
	for line in open(pathname + os.sep + filename, "r"):
		line = line.strip()
		if not line or line.startswith('#'): #Whitespace or comment.
			continue
			
		values = line.split()
		if values[0] == 'newmtl': #New material being defined.
			if mtl_override:
				(override, mtl) = mtl_override[0](values, contents, mtl_override[1], mtl_override[2])
			else:
				mtl = contents[values[1]] = {}
		elif mtl is None:
			raise Exception("'newmtl' must appear at the start of a MTL file, but it was not found")
		elif not override and values[0] == 'Kd': #Only grab diffusion, and only if it hasn't been overridden.
			mtl[values[0]] = map(float, values[1:])
	return contents
	